iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
Rust

30天Rust從零到全端系列 第 10

Day 10: 生命週期:參考的有效性保證

  • 分享至 

  • xImage
  •  

前言

讓我們從一個問題開始:

// 這個函式無法編譯
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

編譯器會報錯:

error[E0106]: missing lifetime specifier
help: this function's return type contains a borrowed value, 
      but the signature does not say whether it is borrowed from `x` or `y`

問題是:編譯器不知道返回的參考是來自 x 還是 y,因此無法確保返回的參考在使用時仍然有效。

生命週期註解

生命週期註解不會改變參考的實際生命週期,它只是告訴編譯器參考之間的關係:

// 'a 讀作 "lifetime a"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

這告訴編譯器:

  • 參數 xy 的生命週期至少要和 'a 一樣長
  • 返回值的生命週期也是 'a
  • 實際的生命週期 'axy 生命週期中較短的那個

生命週期的實際運作

fn main() {
    let string1 = String::from("長字串");
    
    {
        let string2 = String::from("短");
        let result = longest(string1.as_str(), string2.as_str());
        println!("最長的字串是: {}", result);
    } // string2 在這裡被丟棄,result 也不能在這之後使用
    
    // println!("{}", result); // 錯誤!result 的生命週期已經結束
}

視覺化生命週期:

string1 的生命週期: |-------------------------------------|
string2 的生命週期:     |----------|
result 的生命週期:      |---------|  (受限於較短的 string2)

生命週期語法詳解

基本語法

&i32        // 一個參考
&'a i32     // 帶有明確生命週期的參考
&'a mut i32 // 帶有明確生命週期的可變參考

函式中的生命週期

// 單一生命週期參數
fn single_lifetime<'a>(x: &'a str) -> &'a str {
    x
}

// 多個生命週期參數
fn multiple_lifetimes<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    x  // 只返回 x,所以返回值的生命週期是 'a
}

// 不同的生命週期關係
fn complex_lifetimes<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
    // 'b: 'a 表示 'b 的生命週期至少要和 'a 一樣長
    if x.len() > 0 {
        x
    } else {
        y  // 因為 'b >= 'a,所以可以返回 y
    }
}

結構體中的生命週期

當結構體包含參考時,必須指定生命週期:

#[derive(Debug)]
struct TaskRef<'a> {
    title: &'a str,
    description: &'a str,
}

impl<'a> TaskRef<'a> {
    fn new(title: &'a str, description: &'a str) -> Self {
        TaskRef { title, description }
    }
    
    fn get_title(&self) -> &str {
        self.title
    }
}

fn main() {
    let title = String::from("學習生命週期");
    let desc = String::from("理解 Rust 的生命週期機制");
    
    let task = TaskRef::new(&title, &desc);
    println!("任務: {:?}", task);
    
    // title 和 desc 必須活得比 task 久
}

實戰:帶參考的任務管理器

struct TaskManager<'a> {
    name: String,
    current_task: Option<&'a Task>,
}

impl<'a> TaskManager<'a> {
    fn new(name: String) -> Self {
        TaskManager {
            name,
            current_task: None,
        }
    }
    
    fn set_current_task(&mut self, task: &'a Task) {
        self.current_task = Some(task);
    }
    
    fn describe_current(&self) {
        match self.current_task {
            Some(task) => println!("當前任務: {}", task.title),
            None => println!("沒有當前任務"),
        }
    }
}

fn main() {
    let task = Task::new(
        String::from("完成專案"),
        String::from("在截止日期前完成")
    );
    
    let mut manager = TaskManager::new(String::from("專案經理"));
    manager.set_current_task(&task);
    manager.describe_current();
    
    // task 必須活得比 manager 久(或一樣久)
}

生命週期省略規則

Rust 有三個生命週期省略規則,讓你在大多數情況下不需要明確寫出生命週期:

規則 1:每個參考參數都有自己的生命週期

// 你寫的
fn foo(x: &str, y: &str) -> String

// 編譯器看到的
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> String

規則 2:如果只有一個輸入生命週期,它會被賦予所有輸出生命週期

// 你寫的
fn foo(x: &str) -> &str

// 編譯器看到的
fn foo<'a>(x: &'a str) -> &'a str

規則 3:如果有 &self 或 &mut self,self 的生命週期會被賦予所有輸出生命週期

// 你寫的
impl Task {
    fn get_title(&self) -> &str
}

// 編譯器看到的
impl Task {
    fn get_title<'a>(&'a self) -> &'a str
}

靜態生命週期

'static 是一個特殊的生命週期,表示參考在整個程式執行期間都有效:

// 字串字面值都有 'static 生命週期
let s: &'static str = "我是靜態字串";

// 定義全域常數
static GLOBAL_TASK: &str = "全域任務";

// 函式可以要求 'static 生命週期
fn requires_static(s: &'static str) {
    println!("靜態字串: {}", s);
}

fn main() {
    requires_static("這是字串字面值");
    requires_static(GLOBAL_TASK);
    
    let dynamic = String::from("動態字串");
    // requires_static(&dynamic); // 錯誤!dynamic 不是 'static
}

生命週期界限

有時我們需要指定泛型的生命週期界限:

use std::fmt::Display;

// T 必須實作 Display 且生命週期至少和 'a 一樣長
fn longest_with_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("公告: {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

複雜範例:多重生命週期

struct Context<'a> {
    text: &'a str,
}

struct Parser<'a, 'b> {
    context: &'a Context<'b>,
}

impl<'a, 'b> Parser<'a, 'b> {
    fn parse(&self) -> Vec<&'b str> {
        self.context.text.split(',').collect()
    }
}

fn main() {
    let text = String::from("Rust,生命週期,借用");
    let context = Context { text: &text };
    let parser = Parser { context: &context };
    
    let result = parser.parse();
    println!("解析結果: {:?}", result);
}

常見的生命週期模式

1. 返回參數之一

fn first_or_second<'a>(first: &'a str, second: &'a str, use_first: bool) -> &'a str {
    if use_first {
        first
    } else {
        second
    }
}

2. 結構體方法返回欄位

impl<'a> TaskRef<'a> {
    // 返回的生命週期與 self 相同
    fn title(&self) -> &str {
        self.title
    }
}

3. 迭代器模式

struct TaskIterator<'a> {
    tasks: &'a [Task],
    index: usize,
}

impl<'a> Iterator for TaskIterator<'a> {
    type Item = &'a Task;
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.index < self.tasks.len() {
            let task = &self.tasks[self.index];
            self.index += 1;
            Some(task)
        } else {
            None
        }
    }
}

生命週期與所有權的關係

記住這個重要概念:

  • 所有權決定誰負責清理資源
  • 借用允許臨時使用資源
  • 生命週期確保借用是安全的
fn demonstrate_relationship() {
    let owned = String::from("我擁有這個");  // 所有權
    
    {
        let borrowed = &owned;  // 借用開始,生命週期 'a 開始
        println!("{}", borrowed);
    }  // 借用結束,生命週期 'a 結束
    
    drop(owned);  // 所有權結束,值被清理
}

除錯生命週期問題

當遇到生命週期錯誤時,問自己這些問題:

  1. 返回的參考來自哪裡?
  2. 它的生命週期夠長嗎?
  3. 是否應該返回擁有的值而不是參考?
// 常見錯誤:返回局部變數的參考
fn bad_function() -> &String {
    let s = String::from("局部變數");
    &s  // 錯誤!s 會被丟棄
}

// 解決方案 1:返回擁有的值
fn good_function_1() -> String {
    String::from("擁有的值")
}

// 解決方案 2:接收參考並返回它
fn good_function_2(s: &str) -> &str {
    s
}

Follow-up

  1. 基礎練習:修復生命週期錯誤
// 修復這個函式
fn combine_strings(s1: &str, s2: &str) -> &str {
    let result = String::from(s1) + s2;
    &result  // 這裡有問題!
}
  1. 進階練習:實作帶生命週期的快取
struct Cache<'a> {
    data: Option<&'a str>,
}

impl<'a> Cache<'a> {
    fn new() -> Self {
        // 實作
    }
    
    fn set(&mut self, value: &'a str) {
        // 實作
    }
    
    fn get(&self) -> Option<&str> {
        // 實作
    }
}
  1. 挑戰練習:實作字串分割器
struct StrSplitter<'a> {
    remainder: Option<&'a str>,
    delimiter: &'a str,
}

impl<'a> StrSplitter<'a> {
    fn new(string: &'a str, delimiter: &'a str) -> Self {
        // 實作
    }
}

impl<'a> Iterator for StrSplitter<'a> {
    type Item = &'a str;
    
    fn next(&mut self) -> Option<Self::Item> {
        // 實作分割邏輯
    }
}

上一篇
Day 9: 借用與參考:不轉移所有權
下一篇
Day 11: 結構體:組織相關資料
系列文
30天Rust從零到全端15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言